import { join } from 'path';
import { writeFile, stat, unlink, readFile } from 'fs/promises';
import { nextOnPagesVersion, readJsonFile } from '../utils';
import { getPhaseRoutes, getVercelConfig } from './getVercelConfig';
import { cliError } from '../cli';

/**
 * Builds metadata files needed for the worker to correctly run.
 *
 * @param outputDir Output directory for the metadata files.
 * @param opts Options for building metadata files.
 */
export async function buildMetadataFiles(
	outputDir: string,
	opts: BuildMetadataFilesOpts,
) {
	await Promise.all([
		buildNextStaticHeaders(outputDir, opts),
		buildRoutes(outputDir, opts),
	]);
}

/**
 * Builds the `_headers` file, which is used by Cloudflare to determine which headers to add to
 * responses for routes that match the given pattern.
 *
 * @param outputDir Output directory for the metadata files.
 */
async function buildNextStaticHeaders(
	outputDir: string,
	opts: BuildMetadataFilesOpts,
) {
	const vercelConfig = await getVercelConfig();

	const hitRoutes = getPhaseRoutes(vercelConfig.routes ?? [], 'hit');

	const nextStaticPath = getNextStaticDirPath(opts);
	const nextStaticRoute = hitRoutes.find(
		route => route.src?.startsWith(nextStaticPath),
	);
	const nextStaticHeaders = (nextStaticRoute as VercelSource)?.headers;

	if (!nextStaticHeaders) {
		return;
	}

	const headersPath = join(outputDir, '_headers');

	const nopContent = `
# === START AUTOGENERATED @cloudflare/next-on-pages IMMUTABLE HEADERS ===
${nextStaticPath}/*
${Object.entries(nextStaticHeaders)
	.map(([header, value]) => `  ${header}: ${value}`)
	.join('\n')}
# === END AUTOGENERATED @cloudflare/next-on-pages IMMUTABLE HEADERS ===\n`;

	try {
		// Vercel hard links the static files directory:
		// https://github.com/vercel/vercel/blob/8e20fed/packages/cli/src/util/build/write-build-result.ts#L336
		// So we need to unlink before we append data, otherwise we will mutate
		// the original file found in the public directory.
		const stats = await stat(headersPath);
		if (stats.nlink > 1) {
			const data = await readFile(headersPath);
			await unlink(headersPath);
			await writeFile(headersPath, data);
		}
	} catch (e) {
		if ((e as { code?: string }).code !== 'ENOENT') {
			throw e;
		}
	}
	await writeFile(headersPath, nopContent, { flag: 'a' });
}

/**
 * Builds the `_routes.json` file, which is used by Cloudflare to determine which routes should
 * invoke the worker.
 *
 * Collects existing entries from the `_routes.json` file in the root of the project, if it exists.
 *
 * @param outputDir Output directory for the metadata files.
 * @param opts Options for building metadata files.
 */
async function buildRoutes(outputDir: string, opts: BuildMetadataFilesOpts) {
	const nextStaticPath = getNextStaticDirPath(opts);

	const existingFile = await readJsonFile<RoutesJsonFile>('_routes.json');
	if (existingFile && existingFile.version !== 1) {
		cliError(
			`Found _routes.json with version ${existingFile.version}, but only version 1 is supported.`,
		);
		process.exit(1);
	}

	const { include = [], exclude = [] } = existingFile ?? {};
	const includeEntries = include.length > 0 ? include : ['/*'];
	const excludeEntries = new Set([`${nextStaticPath}/*`, ...exclude]);

	try {
		await writeFile(
			join(outputDir, '_routes.json'),
			JSON.stringify({
				version: 1,
				description: `Built with @cloudflare/next-on-pages@${nextOnPagesVersion}.`,
				include: includeEntries,
				exclude: [...excludeEntries],
			}),
		);
	} catch (e) {
		if ((e as { code?: string }).code !== 'EEXIST') {
			throw e;
		}
	}
}

type RoutesJsonFile = {
	version: 1;
	description: string;
	include: string[];
	exclude: string[];
};

/**
 * Finds the path to the `/_next/static` directory from the list of static assets. Accounts for the
 * path being inside sub-directories, e.g. `/blog/_next/static`, and falls back to `/_next/static`.
 *
 * @param opts Options for building metadata files.
 * @returns The path to the `/_next/static` directory.
 */
function getNextStaticDirPath({
	staticAssets,
}: BuildMetadataFilesOpts): string {
	const regex = /^(.*\/_next\/static)\/.+$/;
	const asset = staticAssets.find(a => regex.test(a));
	return asset?.match(regex)?.[1] ?? '/_next/static';
}

type BuildMetadataFilesOpts = {
	staticAssets: string[];
};
